Skip to content

Comments

Added copy to clipboard button for console tab commands#2431

Open
aadeina wants to merge 3 commits intodjango:mainfrom
aadeina:docs-copy-to-clipboard
Open

Added copy to clipboard button for console tab commands#2431
aadeina wants to merge 3 commits intodjango:mainfrom
aadeina:docs-copy-to-clipboard

Conversation

@aadeina
Copy link
Contributor

@aadeina aadeina commented Jan 5, 2026

Summary

Adds a copy-to-clipboard button to console tab code blocks (Unix / Windows installation commands).

Fixes: #1276, #2399
Builds on feedback from @sabderemane in PR #1434.

Changes

JavaScript

djangoproject/static/js/djangoproject.js

  • Extends existing clipboard functionality to support console tabs
  • Adds smart command text extraction that excludes:
    • Shell prompts (.gp — e.g. $, ...>)
    • Command output (.go)
  • Minimal change set (~20 lines added), reusing existing patterns and logic

SCSS

djangoproject/scss/_console-tabs.scss

  • Adds position: relative to console tab sections
  • Introduces .btn-clipboard styling using absolute positioning (no float)
  • Includes hover states and “Copied!” success feedback animation
  • Uses existing CSS variables for light/dark mode compatibility

Visual Comparison

Before

Before: console tabs without copy button

After

Video.Project.mp4

@ulgens
Copy link
Member

ulgens commented Jan 5, 2026

This looks nice 🌻 I'd recommend installing git hooks (you can follow the last section of the readme) and rebasing the PR to have a bit more clear history.

@aadeina aadeina force-pushed the docs-copy-to-clipboard branch from ad9d2d6 to 24e3b21 Compare January 5, 2026 19:16
@aadeina aadeina closed this Jan 5, 2026
@aadeina aadeina force-pushed the docs-copy-to-clipboard branch from bb03309 to 07c155d Compare January 5, 2026 19:39
@aadeina aadeina reopened this Jan 5, 2026
@aadeina aadeina force-pushed the docs-copy-to-clipboard branch from c22de90 to cd6cf4c Compare January 5, 2026 20:04
@aadeina
Copy link
Contributor Author

aadeina commented Jan 5, 2026

Hi @ulgens ,
I’ve cleaned up the branch and ensured the PR now contains only the intended commit.
Please let me know if anything else is needed.

Copy link
Member

@adamzap adamzap left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are some initial thoughts. Thanks for attempting this!

});

function on_success(el) {
function on_success() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why add an unused argument here and on line 145?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Just to clarify — I actually removed the unused el parameter here, since it wasn't being used (the functions access success_el via closure instead). Happy to keep it if you prefer consistency with other patterns in the codebase.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, thanks! Sorry for my misunderstanding here.

// Console tabs: extract text excluding prompts (.gp) and output (.go)
const pre_el = console_section.querySelector('.highlight pre');
text = '';
if (pre_el) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what case is there not a <pre> element?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — since Pygments always generates the <pre> element, this check isn't strictly necessary.
I'll remove it to keep the code cleaner. Thanks for pointing this out!

const pre_el = console_section.querySelector('.highlight pre');
text = '';
if (pre_el) {
pre_el.childNodes.forEach(function (node) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be more elegant to grab the text content like the old code was doing and just left trim the prompt?

(...).textContent.trim().replace(/^\$ /, '')

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great question! I went with the node-based approach mainly because:

  1. It also handles the Windows tab (which uses ...> prompts)
  2. It excludes command output marked with .go (in case any examples show output)

That said, if the console blocks only ever show simple $ prompts without output, a regex would definitely be simpler. I'm happy to switch to:

text = pre_el.textContent.trim().replace(/^(\$ |\.\.\.>)/gm, '');

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know which approach you'd prefer!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's see how the regex looks for now. I should have more time to look at this soon. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated to use the regex approach. Thanks!

@aadeina aadeina force-pushed the docs-copy-to-clipboard branch from cd6cf4c to b3cf678 Compare January 25, 2026 19:44
@aadeina
Copy link
Contributor Author

aadeina commented Jan 25, 2026

Updated per feedback — removed the if check. Thanks! Ready for another look when you have time.

@adamzap
Copy link
Member

adamzap commented Jan 29, 2026

Thank you. In the future, could you wait to squash until the PR is ready? That way I can review new commits in isolation.

@aadeina
Copy link
Contributor Author

aadeina commented Feb 2, 2026

Understood, apologies for the premature squash. I’ll keep commits separate going forward and appreciate the guidance on best practices.

Copy link
Member

@adamzap adamzap left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the changes here. I'm seeing two small design issues with the "Copied!" text on console tabs:

Image
  1. I think the text should use our monospace font to match the "Copied!" text on code snippets
  2. The text is changing the layout vertically, maybe one pixel, when it is present

Would you be able to address these?

document.querySelectorAll('.btn-clipboard').forEach(function (el) {
el.addEventListener('click', function () {
// Remove any existing success message first
const existing = this.querySelector('.clipboard-success');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we call this success_el and drop the comment? I guess we'd need a let.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, I think existing_el or existing_success_el would be fine if you don't want to use the same variable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Renamed to existing_el for consistency.

if (console_section) {
// Console tabs: extract text excluding prompts
const pre_el = console_section.querySelector('.highlight pre');
text = pre_el.textContent.replace(/^\$ |^\.\.\.\\>/gm, '');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we set the regex to a variable and drop the comment here and in the else below? Maybe prompt_regex?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Extracted this to const prompt_regex.

}

navigator.clipboard.writeText(text).then(on_success, on_error);
navigator.clipboard.writeText(text.trim()).then(on_success, on_error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this trim call needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .trim() prevents a trailing newline, so the command doesn't execute immediately upon paste.

@aadeina
Copy link
Contributor Author

aadeina commented Feb 13, 2026

Visual comparison (1px layout shift fix):

Before:

LayoutShiftBefore.mp4

After:

LayoutShiftAfter.mp4

Please let me know if there's anything else you'd like me to adjust!

@aadeina aadeina requested a review from adamzap February 18, 2026 03:10
@adamzap
Copy link
Member

adamzap commented Feb 19, 2026

Thanks for the changes. I'll try to get to this by end of tomorrow!

Copy link
Member

@adamzap adamzap left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Looks great!

Thanks for your patience here. I will ask someone on the team to do a CSS review.

@aadeina
Copy link
Contributor Author

aadeina commented Feb 20, 2026

Thanks so much for the thorough reviews and guidance, @adamzap! I really appreciate the time you took to help refine this. Looking forward to the CSS review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants